//@version=6
// ═══════════════════════════════════════════════════════════════════════════════════════════════
//  ZENITH RSI VOLUME PRO   |   Pine Script® v6   |   Version 1.0.0   |   First written: 19 Oct 2026
// ═══════════════════════════════════════════════════════════════════════════════════════════════
//
//  AUTHOR ............................ Michael_Fx_Trader
//  PUBLISHER ......................... Michael_Fx_Trader
//  COPYRIGHT / RIGHTS OWNER .......... Michael_Fx_Trader
//  COPYRIGHT NOTICE .................. © 2026 Michael_Fx_Trader. All rights reserved.
//
//  ───────────────────────────────  AUTHOR VERIFICATION & DECLARATION  ───────────────────────────────
//  I, Michael_Fx_Trader, declare and confirm the following regarding this script:
//
//   1. AUTHORSHIP      - I am the author and the sole publisher of "Zenith RSI Volume Pro" and I hold all
//                        rights to this script, including its name, source code, logic and design.
//   2. ORIGINALITY     - This is an ORIGINAL implementation. The source code, the scoring model, the
//                        setup-detection rules, the trade lifecycle engine, the target engine, the
//                        statistics engine, the drawing logic and the dashboard were written specifically
//                        for this indicator. No part of it is a copy, clone, fork, translation or
//                        re-skin of any other published or private script.
//   3. NO THIRD-PARTY CODE - It does not include, reproduce or modify any third-party source code, and it
//                        does not use any code from other TradingView authors or open-source scripts.
//   4. STANDARD CONCEPTS - It relies only on publicly known, non-proprietary technical-analysis concepts
//                        (RSI, EMA, ATR, ADX/DMI, relative volume, swing pivots, market structure, R-multiple
//                        risk management). These concepts are general knowledge; their combination and
//                        implementation in this script are my own work.
//   5. HOUSE RULES     - The script is intended to comply with the TradingView House Rules: it is an
//                        analysis tool, it makes no profit guarantees and it does not execute trades.
//   6. HONEST USE      - All levels (ENTRY / SL / TP1-TP4), scores and statistics are technical-analysis
//                        outputs only. They are NOT financial advice, NOT predictions and NOT probabilities
//                        of success. Past signal behaviour does not guarantee future results.
//
//  Redistribution, resale, re-publication, copying or modification of this script without the written
//  permission of the author (Michael_Fx_Trader) is not permitted.
//
//  ──────────────────────────────────────────  DESIGN NOTES  ───────────────────────────────────────────
//  ARCHITECTURE  : One script cannot create two independent lower panes. The script therefore uses ONE
//                  lower pane that contains BOTH the Advanced RSI panel (0-100 scale) and the Smart
//                  Volume panel (a dedicated band under the RSI, scaled -45 to -15). All trade boxes,
//                  entry/SL/TP lines, labels, structure markers, EMAs and the dashboards are drawn on the
//                  MAIN PRICE CHART through force_overlay = true.
//  NON-REPAINT   : All signals, trade creation and trade management run only on confirmed bar closes
//                  (barstate.isconfirmed). Higher-timeframe data uses lookahead_off and the previous
//                  COMPLETED higher-timeframe bar ([1]), so no unfinished HTF candle is ever used.
//                  Pivot-based items (swing structure, divergence) are confirmed with a delay equal to the
//                  selected pivot length - they are never presented as known earlier than they were.
//                  Active trade levels are fixed at creation and never recalculated.
//  VOLUME        : On Forex / many XAUUSD feeds, "volume" is TICK volume, not centralised traded volume.
//                  Volume conclusions are inferences from OHLCV data, not exact institutional order flow.
//                  If a symbol has no usable volume, all volume-dependent confirmation is disabled
//                  automatically (never faked).
//  INTRABAR RULE : If the stop and a target are both touched inside one candle, the intrabar order is
//                  unknown, so the conservative rule is used: STOP is assumed first, and the event is
//                  counted as "ambiguous" in the statistics.
// ═══════════════════════════════════════════════════════════════════════════════════════════════

indicator("Zenith RSI Volume Pro", shorttitle = "Zenith RSI Vol Pro", overlay = false, max_lines_count = 500, max_labels_count = 500, max_boxes_count = 500)

// ═══════════════════════════════════════  PALETTE  ═══════════════════════════════════════
color C_EMERALD = #10B981
color C_TEAL    = #14B8A6
color C_CYAN    = #22D3EE
color C_BLUE    = #3B82F6
color C_CRIMSON = #EF4444
color C_VIOLET  = #8B5CF6
color C_MAGENTA = #D946EF
color C_SLATE   = #64748B
color C_NAVY    = #0B1220
color C_TXT     = #E2E8F0
color C_AMBER   = #F59E0B

// ═══════════════════════════════════════  INPUTS  ═══════════════════════════════════════
string GG  = "1. General"
string GR  = "2. RSI Engine"
string GV  = "3. Smart Volume Engine"
string GM  = "4. Market Structure"
string GS  = "5. Setup Types"
string GT  = "6. Trend & Regime"
string GTM = "7. Trade Management"
string GSE = "8. Session Filter"
string GVI = "9. Visual Style"
string GAL = "10. Alerts"
string GW  = "11. Score Weights"

// --- General
enabled     = input.bool(true, "Enable Signals", group = GG)
sigMode     = input.string("Balanced", "Signal Mode", options = ["Strict Confluence", "Balanced", "Early Entry"], group = GG, tooltip = "Strict: score threshold +5, RSI confirm + positive slope, volume >= threshold, EMA + HTF trend alignment, bullish/bearish structure required.\nBalanced: selected score threshold, selected RSI confirmation, volume >= low threshold.\nEarly: score threshold -10, flexible RSI momentum, no volume or EMA gate. Early signals may be less reliable.")
minScoreIn  = input.int(75, "Minimum Confluence Score", minval = 40, maxval = 100, group = GG, tooltip = "Technical rating only. NOT a probability of success.")
hiConf      = input.int(80, "High-Confluence Threshold", minval = 50, maxval = 100, group = GG)
showBuys    = input.bool(true, "Show BUY Setups", group = GG)
showSells   = input.bool(true, "Show SELL Setups", group = GG)
cooldown    = input.int(5, "Signal Cooldown (bars)", minval = 0, maxval = 200, group = GG)
histKeep    = input.int(5, "Completed Setups Kept On Chart", minval = 0, maxval = 30, group = GG)
tfWarn      = input.bool(true, "Warn If Timeframe Is Not M1-H1", group = GG)

// --- RSI
rsiLen      = input.int(14, "RSI Length", minval = 2, group = GR)
rsiSrc      = input.source(close, "RSI Source", group = GR)
rsiOb       = input.float(70.0, "Overbought Level", group = GR)
rsiUp       = input.float(60.0, "Upper Momentum Zone", group = GR)
rsiMid      = input.float(50.0, "Midpoint", group = GR)
rsiLo       = input.float(40.0, "Lower Momentum Zone", group = GR)
rsiOs       = input.float(30.0, "Oversold Level", group = GR)
momLook     = input.int(3, "Momentum Lookback (bars)", minval = 1, maxval = 20, group = GR)
rsiConfMode = input.string("Flexible Momentum", "RSI Confirmation Mode", options = ["Midpoint Cross", "Sustained Above/Below Midpoint", "Flexible Momentum"], group = GR, tooltip = "Midpoint Cross: RSI crossed 50 within the window below.\nSustained: RSI stayed above (below) 50 for two bars.\nFlexible: above/below 50, or rising/falling from the lower/upper zone with acceleration.")
crossWin    = input.int(3, "Midpoint Cross Window (bars)", minval = 1, maxval = 20, group = GR)
divOn       = input.bool(true, "Detect RSI Divergence", group = GR, tooltip = "Pivot-based. Confirmed with a delay equal to the pivot length. Divergence alone is never a trade trigger.")
divPivot    = input.int(5, "Divergence Pivot Length", minval = 2, maxval = 20, group = GR)
divMaxBars  = input.int(60, "Max Bars Between Divergence Pivots", minval = 10, maxval = 300, group = GR)

// --- Volume
volMaType   = input.string("SMA", "Volume Average Type", options = ["SMA", "EMA", "WMA", "RMA"], group = GV)
volLen      = input.int(20, "Volume Average Length", minval = 2, group = GV)
rvolLow     = input.float(0.80, "Low Activity Below (RVOL)", step = 0.05, group = GV)
rvolThr     = input.float(1.20, "Elevated RVOL Threshold", step = 0.05, group = GV)
rvolStrong  = input.float(1.50, "Strong RVOL Threshold", step = 0.05, group = GV)
rvolExc     = input.float(2.00, "Exceptional RVOL Threshold", step = 0.05, group = GV)
volReq      = input.string("Gate + Score", "Volume Requirement", options = ["Gate + Score", "Score Only"], group = GV, tooltip = "Gate + Score: mode-specific RVOL gate AND score contribution.\nScore Only: low volume only lowers the score; it never blocks a setup by itself.")
volAnomOn   = input.bool(true, "Volume Anomaly Markers", group = GV)
volAnomZ    = input.float(2.5, "Anomaly Z-Score", step = 0.1, group = GV)
volPanel    = input.bool(true, "Show Smart Volume Panel", group = GV, tooltip = "Tick volume on Forex / many XAUUSD feeds - not centralised traded volume.")
volHL       = input.bool(true, "Highlight Exceptional Volume (Cyan)", group = GV)
volCap      = input.float(3.0, "RVOL Value That Fills The Panel", minval = 1.5, step = 0.5, group = GV)

// --- Structure
swLen       = input.int(5, "Swing Length", minval = 2, maxval = 30, group = GM, tooltip = "Pivots are confirmed after this many bars (delay).")
bosOn       = input.bool(true, "Use Break Of Structure (BOS)", group = GM, tooltip = "BOS = close (or wick) beyond the latest confirmed swing in the direction of the prevailing structure.")
chochOn     = input.bool(true, "Use Change Of Character (CHoCH)", group = GM, tooltip = "CHoCH = first close (or wick) beyond the latest confirmed swing AGAINST the prevailing structure (structure flips).")
wickBreak   = input.bool(false, "Wick Break Mode (default: close break)", group = GM)
sweepOn     = input.bool(true, "Detect Liquidity Sweeps", group = GM, tooltip = "Price-action interpretation only; not proof that stop orders were triggered.")
sweepWick   = input.float(0.35, "Sweep Min Rejection Wick (fraction of range)", minval = 0.0, maxval = 1.0, step = 0.05, group = GM)
retestWin   = input.int(20, "Retest Window (bars)", minval = 3, maxval = 100, group = GM)
retestTol   = input.float(0.30, "Retest Tolerance (ATR)", minval = 0.05, step = 0.05, group = GM)
srOn        = input.bool(true, "Show Support / Resistance Lines", group = GM)
maxLvls     = input.int(4, "Max S/R Lines Per Side", minval = 1, maxval = 20, group = GM)
swingLbl    = input.bool(false, "Show HH / HL / LH / LL Labels", group = GM)
showStruct  = input.bool(true, "Show BOS / CHoCH Markers", group = GM)

// --- Setup types
setA        = input.bool(true, "A: Breakout / Breakdown", group = GS)
setB        = input.bool(true, "B: Breakout Retest", group = GS)
setC        = input.bool(true, "C: Liquidity Sweep Reversal", group = GS)
setD        = input.bool(true, "D: Trend Continuation Pullback", group = GS)
extAtr      = input.float(2.0, "Max Signal Candle Range (ATR)", minval = 0.5, step = 0.1, group = GS, tooltip = "Breakout candles larger than this are treated as over-extended.")

// --- Trend / regime
emaFLen     = input.int(9, "Fast EMA", minval = 1, group = GT)
emaSLen     = input.int(21, "Slow EMA", minval = 1, group = GT)
emaTLen     = input.int(50, "Trend EMA", minval = 1, group = GT)
showEma     = input.bool(true, "Show EMAs On Chart", group = GT)
adxOn       = input.bool(true, "Use ADX Regime Filter", group = GT)
adxLen      = input.int(14, "ADX Length", minval = 2, group = GT)
adxThr      = input.float(22.0, "ADX Trend Threshold", minval = 10, maxval = 40, group = GT)
atrLen      = input.int(14, "ATR Length", minval = 2, group = GT)
atrVolOn    = input.bool(true, "Block Unstable High Volatility", group = GT)
unstableMul = input.float(2.2, "Unstable If ATR > Avg ATR x", minval = 1.2, step = 0.1, group = GT)
rangeBeh    = input.string("Boundary Only", "Range Market Behavior", options = ["Allow All", "Boundary Only", "Block All Signals In Range"], group = GT, tooltip = "In a range regime: Boundary Only blocks mid-range signals and continuation (D) setups; Block All suppresses every signal.")
htfOn       = input.bool(true, "Higher-Timeframe Confirmation", group = GT, tooltip = "Uses the previous COMPLETED HTF bar (lookahead off). Adds a delay until the HTF bar closes.")
htfTf       = input.timeframe("60", "HTF Timeframe", group = GT)
htfEmaLen   = input.int(50, "HTF EMA Length", minval = 2, group = GT)

// --- Trade management
entryMethod = input.string("Signal Close", "Entry Method", options = ["Signal Close", "Breakout Level", "Retest Confirmation"], group = GTM, tooltip = "Signal Close: entry = close of the confirmed signal candle (a reference price, not a guaranteed fill).\nBreakout Level: breakout setups become a PENDING limit order at the broken level and only activate if price returns to it.\nRetest Confirmation: breakout setup A is disabled; only retest / sweep / continuation setups fire.")
stopMethod  = input.string("Hybrid", "Stop-Loss Method", options = ["Hybrid", "Structure", "ATR"], group = GTM, tooltip = "Hybrid: confirmed swing + ATR buffer; falls back to the ATR stop if the structural stop is too wide.")
stopAtrMult = input.float(1.5, "ATR Stop Multiplier", minval = 0.3, step = 0.1, group = GTM)
stopBuf     = input.float(0.20, "Structure ATR Buffer", minval = 0.0, step = 0.05, group = GTM)
minTickBuf  = input.int(2, "Minimum Tick Buffer", minval = 0, group = GTM)
minStopAtr  = input.float(0.5, "Minimum Stop Distance (ATR)", minval = 0.1, step = 0.1, group = GTM)
maxStopAtr  = input.float(3.0, "Maximum Stop Distance (ATR)", minval = 0.5, step = 0.1, group = GTM)
tpMode      = input.string("Hybrid", "Target Method", options = ["Hybrid", "R-Multiple", "Structure"], group = GTM, tooltip = "R-Multiple: fixed multiples of risk.\nStructure: nearest confirmed swing levels beyond the previous target.\nHybrid: snaps to a structure level near the R-multiple, otherwise uses the R-multiple.")
tp1m        = input.float(1.0, "TP1 (R)", minval = 0.2, step = 0.1, group = GTM)
tp2m        = input.float(2.0, "TP2 (R)", minval = 0.3, step = 0.1, group = GTM)
tp3m        = input.float(3.0, "TP3 (R)", minval = 0.4, step = 0.1, group = GTM)
tp4m        = input.float(4.0, "TP4 (R)", minval = 0.5, step = 0.1, group = GTM)
minRR       = input.float(1.5, "Minimum Risk-To-Reward (measured to TP2)", minval = 0.5, step = 0.1, group = GTM)
expiryBars  = input.int(100, "Setup Expiry (bars after activation, 0 = off)", minval = 0, group = GTM)
pendExp     = input.int(10, "Pending Order Expiry (bars)", minval = 1, group = GTM)
policy      = input.string("Ignore New Signals", "Active Setup Policy", options = ["Ignore New Signals", "Replace On Opposite Signal", "Close On Opposite Signal"], group = GTM, tooltip = "Only one setup is active at a time. A new signal never overwrites an active setup silently.")
invalOpp    = input.bool(true, "Invalidate If Opposite Structure Break Before TP1", group = GTM)

// --- Session
sessOn      = input.bool(false, "Enable Session Filter", group = GSE)
londonOn    = input.bool(true, "London Session", group = GSE)
nyOn        = input.bool(true, "New York Session", group = GSE)
overlapOnly = input.bool(false, "London/New York Overlap Only", group = GSE)
londonSess  = input.session("0800-1630", "London Hours", group = GSE)
londonTz    = input.string("Europe/London", "London Timezone", options = ["Europe/London", "UTC", "Asia/Karachi", "America/New_York"], group = GSE, tooltip = "Named timezones follow daylight-saving changes automatically.")
nySess      = input.session("0800-1700", "New York Hours", group = GSE)
nyTz        = input.string("America/New_York", "New York Timezone", options = ["America/New_York", "UTC", "Asia/Karachi", "Europe/London"], group = GSE)

// --- Visual
buyCol      = input.color(#10B981, "BUY Color", group = GVI)
sellCol     = input.color(#EF4444, "SELL Color", group = GVI)
entryCol    = input.color(#22D3EE, "Entry Line Color", group = GVI)
slCol       = input.color(#EF4444, "Stop-Loss Color", group = GVI)
tpCol       = input.color(#14B8A6, "Target Color", group = GVI)
lblSizeIn   = input.string("small", "Label Size", options = ["tiny", "small", "normal"], group = GVI)
lineStyleIn = input.string("Dashed", "Level Line Style", options = ["Solid", "Dashed", "Dotted"], group = GVI)
lineW       = input.int(1, "Line Width", minval = 1, maxval = 4, group = GVI)
boxTransp   = input.int(86, "Box Transparency", minval = 40, maxval = 99, group = GVI)
glowOn      = input.bool(true, "Glow-Like Entry Line", group = GVI)
boxW        = input.int(25, "Setup Box Width (bars)", minval = 5, maxval = 200, group = GVI)
showLabels  = input.bool(true, "Show Level Price Labels", group = GVI)
showTp1     = input.bool(true, "Show TP1", group = GVI)
showTp2     = input.bool(true, "Show TP2", group = GVI)
showTp3     = input.bool(true, "Show TP3", group = GVI)
showTp4     = input.bool(true, "Show TP4", group = GVI)

// --- Dashboard (single, movable, scalable)
string GD   = "12. Dashboard"
showDash    = input.bool(true, "Show Dashboard", group = GD)
dashAnchor  = input.string("Top Right", "Anchor Position", options = ["Top Left", "Top Center", "Top Right", "Middle Left", "Middle Center", "Middle Right", "Bottom Left", "Bottom Center", "Bottom Right"], group = GD, tooltip = "TradingView tables can only anchor to these 9 screen positions. Use the two offsets below to move the dashboard anywhere on the chart.")
dashOffX    = input.float(0.0, "Move Horizontally (% of chart width)", minval = 0.0, maxval = 95.0, step = 1.0, group = GD, tooltip = "Moves the dashboard away from its anchor edge. Left anchors move right, right anchors move left.")
dashOffY    = input.float(0.0, "Move Vertically (% of chart height)", minval = 0.0, maxval = 95.0, step = 1.0, group = GD, tooltip = "Moves the dashboard away from its anchor edge. Top anchors move down, bottom anchors move up.")
dashScale   = input.int(100, "Dashboard Size (%)", minval = 40, maxval = 300, step = 5, group = GD, tooltip = "100% = default size. 50% = half size, 150% = one and a half times bigger.")
dashTransp  = input.int(8, "Background Transparency", minval = 0, maxval = 100, group = GD)
showStats   = input.bool(true, "Include Signal Statistics Section", group = GD)

// --- Alerts
dynAlerts   = input.bool(true, "Send Dynamic alert() Messages", group = GAL, tooltip = "Create the alert with 'Any alert() function call' to receive full messages (symbol, timeframe, entry, SL, TP1-4, score, RVOL, RSI). The separate alertcondition() entries use fixed text with placeholders.")

// --- Weights
wRsi        = input.float(25.0, "RSI Weight", minval = 0.0, group = GW)
wVol        = input.float(20.0, "Relative Volume Weight", minval = 0.0, group = GW)
wStr        = input.float(25.0, "Market Structure Weight", minval = 0.0, group = GW)
wTrend      = input.float(15.0, "Trend / Regime Weight", minval = 0.0, group = GW)
wCandle     = input.float(15.0, "Candle / Volatility Weight", minval = 0.0, group = GW, tooltip = "Weights are normalised automatically. If the symbol has no volume, the volume weight is removed and the score is rescaled.")

// ═══════════════════════════════════════  HELPERS  ═══════════════════════════════════════
f_size(string s) =>
    switch s
        "tiny" => size.tiny
        "normal" => size.normal
        => size.small

f_pos(string s) =>
    switch s
        "Top Left" => position.top_left
        "Bottom Right" => position.bottom_right
        "Bottom Left" => position.bottom_left
        "Middle Right" => position.middle_right
        "Middle Left" => position.middle_left
        => position.top_right

f_style(string s) =>
    s == "Solid" ? line.style_solid : s == "Dotted" ? line.style_dotted : line.style_dashed

f_b2f(bool b) => b ? 1.0 : 0.0

f_cl(float x) => math.max(0.0, math.min(1.0, nz(x)))

f_recent(int b, int n) => not na(b) and bar_index - b <= n

f_rating(float s) =>
    s >= 80 ? "High Confluence" : s >= 70 ? "Strong" : s >= 55 ? "Moderate" : "Weak"

// Target picker: R-multiple base, optionally snapped to a confirmed structure level.
f_tgt(int d, float entry, float risk, float mult, float prevTp, array<float> lv, string mode, float atrv) =>
    float base = entry + d * risk * mult
    float gap = 0.25 * risk
    float res = base
    if mode != "R-Multiple"
        float best = na
        float bestDist = na
        for i = 0 to array.size(lv) - 1
            float lvl = array.get(lv, i)
            bool beyond = d * (lvl - prevTp) >= gap
            bool inWin = mode == "Structure" ? true : math.abs(lvl - base) <= 0.5 * risk
            if beyond and inWin
                float dist = mode == "Structure" ? d * (lvl - prevTp) : math.abs(lvl - base)
                if na(bestDist) or dist < bestDist
                    bestDist := dist
                    best := lvl
        if not na(best)
            res := best - d * 0.05 * nz(atrv)
    if d * (res - prevTp) < gap
        res := base
    if d * (res - prevTp) < gap
        res := prevTp + d * 0.5 * risk
    res

f_lbl(string nm, float px, float r) =>
    nm + "  " + str.tostring(px, format.mintick) + "  (" + str.tostring(r, "#.0") + "R)"


// ═══════════════════════════════════════  CORE SERIES  ═══════════════════════════════════════
float atrV    = ta.atr(atrLen)
float atrAvg  = ta.sma(atrV, 50)
float rng     = high - low
float body    = math.abs(close - open)
float lowLow3 = ta.lowest(low, 3)
float highHigh3 = ta.highest(high, 3)

// ═══════════════════════════════════════  1) RSI MOMENTUM ENGINE  ═══════════════════════════════════════
float rsiV      = ta.rsi(rsiSrc, rsiLen)
float rsiSlope  = rsiV - rsiV[momLook]
float rsiAccel  = (rsiV - rsiV[1]) - (rsiV[1] - rsiV[2])
int   barsUp    = ta.barssince(ta.crossover(rsiV, rsiMid))
int   barsDn    = ta.barssince(ta.crossunder(rsiV, rsiMid))
bool  crossUpRecent = not na(barsUp) and barsUp <= crossWin
bool  crossDnRecent = not na(barsDn) and barsDn <= crossWin

bool bullSust = rsiV > rsiMid and rsiV[1] > rsiMid
bool bearSust = rsiV < rsiMid and rsiV[1] < rsiMid
bool bullFlex = rsiV > rsiMid or (rsiV > rsiLo and rsiSlope > 0 and rsiAccel > 0)
bool bearFlex = rsiV < rsiMid or (rsiV < rsiUp and rsiSlope < 0 and rsiAccel < 0)

bool rsiBullConf = rsiConfMode == "Midpoint Cross" ? (crossUpRecent and rsiV > rsiMid) : rsiConfMode == "Sustained Above/Below Midpoint" ? bullSust : bullFlex
bool rsiBearConf = rsiConfMode == "Midpoint Cross" ? (crossDnRecent and rsiV < rsiMid) : rsiConfMode == "Sustained Above/Below Midpoint" ? bearSust : bearFlex

bool rsiBullTrig = rsiV > rsiMid and rsiSlope > 0
bool rsiBearTrig = rsiV < rsiMid and rsiSlope < 0
// Recovery / rejection context (NOT an automatic trigger - overbought/oversold is context only)
bool rsiRecov  = rsiSlope > 0 and ta.lowest(rsiV, 10) <= rsiLo
bool rsiReject = rsiSlope < 0 and ta.highest(rsiV, 10) >= rsiUp

// --- RSI divergence (pivot based, confirmed with a delay of divPivot bars)
float plR = ta.pivotlow(rsiV, divPivot, divPivot)
float phR = ta.pivothigh(rsiV, divPivot, divPivot)
var float pPLr = na
var float pPLp = na
var int   pPLb = na
var float pPHr = na
var float pPHp = na
var int   pPHb = na
var int   lastBullDivBar = na
var int   lastBearDivBar = na
bool bullDivNow = false
bool bearDivNow = false
if divOn and not na(plR)
    float pxL = low[divPivot]
    int bxL = bar_index - divPivot
    if not na(pPLr) and pxL < pPLp and plR > pPLr and (bxL - pPLb) <= divMaxBars
        bullDivNow := true
        lastBullDivBar := bar_index
        line.new(pPLb, pPLr, bxL, plR, color = C_CYAN, width = 2)
        label.new(bxL, plR, "Bull Div", style = label.style_label_up, color = color.new(C_CYAN, 20), textcolor = C_NAVY, size = size.tiny)
    pPLr := plR
    pPLp := pxL
    pPLb := bxL
if divOn and not na(phR)
    float pxH = high[divPivot]
    int bxH = bar_index - divPivot
    if not na(pPHr) and pxH > pPHp and phR < pPHr and (bxH - pPHb) <= divMaxBars
        bearDivNow := true
        lastBearDivBar := bar_index
        line.new(pPHb, pPHr, bxH, phR, color = C_MAGENTA, width = 2)
        label.new(bxH, phR, "Bear Div", style = label.style_label_down, color = color.new(C_MAGENTA, 20), textcolor = C_NAVY, size = size.tiny)
    pPHr := phR
    pPHp := pxH
    pPHb := bxH
bool recentBullDiv = f_recent(lastBullDivBar, 15)
bool recentBearDiv = f_recent(lastBearDivBar, 15)

string rsiClass = rsiV >= rsiOb ? "Overbought" : rsiV >= rsiUp ? "Upper Momentum" : rsiV >= rsiMid ? "Bullish Side" : rsiV >= rsiLo ? "Bearish Side" : rsiV > rsiOs ? "Lower Momentum" : "Oversold"

// ═══════════════════════════════════════  2) SMART RELATIVE VOLUME ENGINE  ═══════════════════════════════════════
float vol     = nz(volume)
float volSma  = ta.sma(vol, volLen)
float volEma  = ta.ema(vol, volLen)
float volWma  = ta.wma(vol, volLen)
float volRma  = ta.rma(vol, volLen)
float avgVol  = volMaType == "EMA" ? volEma : volMaType == "WMA" ? volWma : volMaType == "RMA" ? volRma : volSma
bool  hasVol  = ta.cum(vol) > 0 and nz(avgVol) > 0
float rvol    = hasVol ? vol / avgVol : na
float volStd  = ta.stdev(vol, volLen)
float volZ    = hasVol and volStd > 0 ? (vol - avgVol) / volStd : na
bool  volAnom = volAnomOn and hasVol and nz(volZ) >= volAnomZ

string volClass = not hasVol ? "No Volume Data" : rvol < rvolLow ? "Very Low Activity" : rvol < rvolThr ? "Normal Activity" : rvol < rvolStrong ? "Elevated Activity" : rvol < rvolExc ? "High Participation" : "Exceptional Activity"
bool  volAbsorb = hasVol and rvol >= rvolStrong and body < 0.35 * nz(atrV)   // large volume, limited price progress (inference only)
bool  volExpand = hasVol and vol > nz(vol[1]) and rvol >= rvolThr

bool strongBullPart = rsiV > rsiMid and rsiSlope > 0 and hasVol and rvol >= rvolStrong and close > open
bool strongBearPart = rsiV < rsiMid and rsiSlope < 0 and hasVol and rvol >= rvolStrong and close < open
bool bullMomState   = rsiV > rsiMid and rsiSlope > 0 and close >= open
bool bearMomState   = rsiV < rsiMid and rsiSlope < 0 and close <= open
bool weakMomState   = (rsiV > rsiMid and (close < open or (hasVol and rvol < rvolLow))) or (rsiV < rsiMid and (close > open or (hasVol and rvol < rvolLow)))
string momState = strongBullPart ? "Strong Bullish Participation" : strongBearPart ? "Strong Bearish Participation" : weakMomState ? "Weak Momentum" : bullMomState ? "Bullish Momentum" : bearMomState ? "Bearish Momentum" : "Neutral / Choppy"

// ═══════════════════════════════════════  3) MARKET STRUCTURE ENGINE  ═══════════════════════════════════════
float pvH = ta.pivothigh(high, swLen, swLen)
float pvL = ta.pivotlow(low, swLen, swLen)
var float lastSH = na
var float lastSL = na
var bool  shBroken = true
var bool  slBroken = true
var int   structDir = 0
var float bkLvlUp = na
var int   bkBarUp = na
var float bkLvlDn = na
var int   bkBarDn = na
var int   lastBullBrk = na
var int   lastBearBrk = na
var int   lastBullEvt = na
var int   lastBearEvt = na
var array<float> shArr = array.new<float>()
var array<float> slArr = array.new<float>()
var line curSHLine = na
var line curSLLine = na
var array<line> srLines = array.new<line>()

bool hhNow = false
bool lhNow = false
bool hlNow = false
bool llNow = false
if not na(pvH)
    hhNow := not na(lastSH) and pvH > lastSH
    lhNow := not na(lastSH) and pvH < lastSH
    lastSH := pvH
    shBroken := false
    array.push(shArr, pvH)
    if array.size(shArr) > 30
        array.shift(shArr)
    if srOn
        if not na(curSHLine)
            line.set_extend(curSHLine, extend.none)
            line.set_x2(curSHLine, bar_index)
        curSHLine := line.new(bar_index - swLen, pvH, bar_index, pvH, extend = extend.right, color = color.new(C_CRIMSON, 35), style = line.style_dotted, width = 1, force_overlay = true)
        array.push(srLines, curSHLine)
        if array.size(srLines) > maxLvls * 2
            line.delete(array.shift(srLines))
if not na(pvL)
    hlNow := not na(lastSL) and pvL > lastSL
    llNow := not na(lastSL) and pvL < lastSL
    lastSL := pvL
    slBroken := false
    array.push(slArr, pvL)
    if array.size(slArr) > 30
        array.shift(slArr)
    if srOn
        if not na(curSLLine)
            line.set_extend(curSLLine, extend.none)
            line.set_x2(curSLLine, bar_index)
        curSLLine := line.new(bar_index - swLen, pvL, bar_index, pvL, extend = extend.right, color = color.new(C_EMERALD, 35), style = line.style_dotted, width = 1, force_overlay = true)
        array.push(srLines, curSLLine)
        if array.size(srLines) > maxLvls * 2
            line.delete(array.shift(srLines))

// Break detection (BOS = with prevailing structure, CHoCH = against it)
bool bullBrk = not shBroken and not na(lastSH) and (wickBreak ? high : close) > lastSH
bool bearBrk = not slBroken and not na(lastSL) and (wickBreak ? low : close) < lastSL
bool bosBull   = bullBrk and structDir >= 0
bool chochBull = bullBrk and structDir == -1
bool bosBear   = bearBrk and structDir <= 0
bool chochBear = bearBrk and structDir == 1
bool bullBrkEvt = bullBrk and ((chochBull and chochOn) or (bosBull and bosOn))
bool bearBrkEvt = bearBrk and ((chochBear and chochOn) or (bosBear and bosOn))
if bullBrk
    shBroken := true
    structDir := 1
    bkLvlUp := lastSH
    bkBarUp := bar_index
    lastBullBrk := bar_index
    if srOn and not na(curSHLine)
        line.set_extend(curSHLine, extend.none)
        line.set_x2(curSHLine, bar_index)
if bearBrk
    slBroken := true
    structDir := -1
    bkLvlDn := lastSL
    bkBarDn := bar_index
    lastBearBrk := bar_index
    if srOn and not na(curSLLine)
        line.set_extend(curSLLine, extend.none)
        line.set_x2(curSLLine, bar_index)

// Retests of broken levels
bool retestBull = false
bool retestBear = false
if not na(bkLvlUp) and bar_index > bkBarUp
    if bar_index - bkBarUp > retestWin or close < bkLvlUp - nz(atrV) * retestTol
        bkLvlUp := na
    else if low <= bkLvlUp + nz(atrV) * retestTol and low >= bkLvlUp - nz(atrV) * retestTol and close > bkLvlUp and close > open
        retestBull := true
        bkLvlUp := na
if not na(bkLvlDn) and bar_index > bkBarDn
    if bar_index - bkBarDn > retestWin or close > bkLvlDn + nz(atrV) * retestTol
        bkLvlDn := na
    else if high >= bkLvlDn - nz(atrV) * retestTol and high <= bkLvlDn + nz(atrV) * retestTol and close < bkLvlDn and close < open
        retestBear := true
        bkLvlDn := na

// Liquidity sweeps (price-action interpretation only)
float lowWick = math.min(open, close) - low
float upWick  = high - math.max(open, close)
bool sweepBull = sweepOn and not na(lastSL) and low < lastSL and close > lastSL and rng > 0 and lowWick / rng >= sweepWick
bool sweepBear = sweepOn and not na(lastSH) and high > lastSH and close < lastSH and rng > 0 and upWick / rng >= sweepWick
if retestBull or sweepBull
    lastBullEvt := bar_index
if retestBear or sweepBear
    lastBearEvt := bar_index

// ═══════════════════════════════════════  4) TREND & REGIME ENGINE  ═══════════════════════════════════════
float emaF = ta.ema(close, emaFLen)
float emaS = ta.ema(close, emaSLen)
float emaT = ta.ema(close, emaTLen)
[diP, diM, adxV] = ta.dmi(adxLen, adxLen)
bool trending = not adxOn or nz(adxV) >= adxThr
bool inRange  = adxOn and nz(adxV) < adxThr
bool lowMom   = adxOn and nz(adxV) < 15
bool highVol  = atrVolOn and nz(atrV) > nz(atrAvg) * unstableMul
float rangeHi = ta.highest(high, 50)
float rangeLo = ta.lowest(low, 50)
bool midRange = close > rangeLo + (rangeHi - rangeLo) * 0.25 and close < rangeHi - (rangeHi - rangeLo) * 0.25
bool emaBull  = emaF > emaS and close > emaT
bool emaBear  = emaF < emaS and close < emaT
string regime = highVol ? "Unstable Volatility" : trending and adxOn ? (emaBull ? "Bullish Trend" : emaBear ? "Bearish Trend" : "Trend (Mixed)") : lowMom ? "Low Momentum" : inRange ? "Range-Bound" : (emaBull ? "Bullish Bias" : emaBear ? "Bearish Bias" : "Neutral")

// Higher-timeframe confirmation (previous COMPLETED HTF bar, lookahead off)
[htfC, htfE] = request.security(syminfo.tickerid, htfTf, [close[1], ta.ema(close, htfEmaLen)[1]], lookahead = barmerge.lookahead_off)
bool htfBullOk = not htfOn or htfC > htfE
bool htfBearOk = not htfOn or htfC < htfE

// Session filter (DST-aware named timezones)
bool inLondon = not na(time(timeframe.period, londonSess, londonTz))
bool inNY     = not na(time(timeframe.period, nySess, nyTz))
bool sessOk   = not sessOn or (overlapOnly ? (inLondon and inNY) : ((londonOn and inLondon) or (nyOn and inNY)))

// ═══════════════════════════════════════  5) PRICE ACTION & CONFIRMATION  ═══════════════════════════════════════
bool bullCandle = close > open and rng > 0 and body >= rng * 0.4
bool bearCandle = close < open and rng > 0 and body >= rng * 0.4
bool extended   = rng > extAtr * nz(atrV, rng)
bool pbBull = ta.lowest(low, 3) <= emaS + nz(atrV) * 0.25
bool pbBear = ta.highest(high, 3) >= emaS - nz(atrV) * 0.25

bool isStrict = sigMode == "Strict Confluence"
bool isEarly  = sigMode == "Early Entry"
bool rangeDBlock = inRange and rangeBeh != "Allow All"

// Setup patterns (A breakout, B retest, C sweep, D continuation)
bool patABull = setA and entryMethod != "Retest Confirmation" and bullBrkEvt and bullCandle and not extended
bool patBBull = setB and retestBull
bool patCBull = setC and sweepBull and close > open
bool patDBull = setD and not rangeDBlock and structDir == 1 and pbBull and close > emaF and bullCandle and rsiBullTrig
bool patBull  = patABull or patBBull or patCBull or patDBull
bool patABear = setA and entryMethod != "Retest Confirmation" and bearBrkEvt and bearCandle and not extended
bool patBBear = setB and retestBear
bool patCBear = setC and sweepBear and close < open
bool patDBear = setD and not rangeDBlock and structDir == -1 and pbBear and close < emaF and bearCandle and rsiBearTrig
bool patBear  = patABear or patBBear or patCBear or patDBear

// ═══════════════════════════════════════  6) CONFLUENCE SCORING ENGINE  ═══════════════════════════════════════
float volScaled = hasVol ? f_cl((rvol - rvolLow) / (rvolStrong - rvolLow)) : 0.0

// Bullish and bearish scores are computed separately - conflicting factors are never combined.
float bRsi = 0.35 * f_b2f(rsiBullConf) + 0.25 * f_b2f(rsiSlope > 0) + 0.20 * f_b2f(rsiV > rsiMid and rsiV < rsiOb) + 0.10 * f_b2f(rsiAccel > 0) + 0.10 * f_b2f(recentBullDiv or rsiRecov)
float sRsi = 0.35 * f_b2f(rsiBearConf) + 0.25 * f_b2f(rsiSlope < 0) + 0.20 * f_b2f(rsiV < rsiMid and rsiV > rsiOs) + 0.10 * f_b2f(rsiAccel < 0) + 0.10 * f_b2f(recentBearDiv or rsiReject)
float bVol = 0.60 * volScaled + 0.25 * f_b2f(close > open) + 0.15 * f_b2f(vol > nz(vol[1]))
float sVol = 0.60 * volScaled + 0.25 * f_b2f(close < open) + 0.15 * f_b2f(vol > nz(vol[1]))
float bStr = 0.35 * f_b2f(structDir == 1) + 0.35 * f_b2f(f_recent(lastBullBrk, 15)) + 0.30 * f_b2f(f_recent(lastBullEvt, 5) or patDBull)
float sStr = 0.35 * f_b2f(structDir == -1) + 0.35 * f_b2f(f_recent(lastBearBrk, 15)) + 0.30 * f_b2f(f_recent(lastBearEvt, 5) or patDBear)
float bTrd = 0.30 * f_b2f(emaF > emaS) + 0.15 * f_b2f(close > emaT) + 0.20 * f_b2f(adxOn ? (nz(adxV) >= adxThr and diP > diM) : emaF > nz(emaF[1])) + 0.25 * f_b2f(htfOn ? htfC > htfE : emaF > nz(emaF[1])) + 0.10 * f_b2f(emaT > nz(emaT[1]))
float sTrd = 0.30 * f_b2f(emaF < emaS) + 0.15 * f_b2f(close < emaT) + 0.20 * f_b2f(adxOn ? (nz(adxV) >= adxThr and diM > diP) : emaF < nz(emaF[1])) + 0.25 * f_b2f(htfOn ? htfC < htfE : emaF < nz(emaF[1])) + 0.10 * f_b2f(emaT < nz(emaT[1]))
float bCan = 0.50 * f_b2f(bullCandle) + 0.30 * f_b2f(not extended) + 0.20 * f_b2f(not highVol)
float sCan = 0.50 * f_b2f(bearCandle) + 0.30 * f_b2f(not extended) + 0.20 * f_b2f(not highVol)

float wVolEff = hasVol ? wVol : 0.0
float wTot = wRsi + wVolEff + wStr + wTrend + wCandle
float scoreBull = wTot > 0 ? 100.0 * (wRsi * bRsi + wVolEff * bVol + wStr * bStr + wTrend * bTrd + wCandle * bCan) / wTot : 0.0
float scoreBear = wTot > 0 ? 100.0 * (wRsi * sRsi + wVolEff * sVol + wStr * sStr + wTrend * sTrd + wCandle * sCan) / wTot : 0.0
scoreBull := math.max(0.0, math.min(100.0, scoreBull))
scoreBear := math.max(0.0, math.min(100.0, scoreBear))
int minScoreEff = math.max(40, math.min(100, minScoreIn + (isStrict ? 5 : isEarly ? -10 : 0)))

// ═══════════════════════════════════════  7) SIGNAL GATES  ═══════════════════════════════════════
bool rsiGateBull = isEarly ? bullFlex : isStrict ? (rsiBullConf and rsiSlope > 0) : rsiBullConf
bool rsiGateBear = isEarly ? bearFlex : isStrict ? (rsiBearConf and rsiSlope < 0) : rsiBearConf
bool rsiOkBull = rsiGateBull or (patCBull and rsiRecov)
bool rsiOkBear = rsiGateBear or (patCBear and rsiReject)

bool volGateBull = not hasVol or volReq == "Score Only" or isEarly or (isStrict ? (rvol >= rvolThr and close > open) : rvol >= rvolLow)
bool volGateBear = not hasVol or volReq == "Score Only" or isEarly or (isStrict ? (rvol >= rvolThr and close < open) : rvol >= rvolLow)

bool trendGateBull = htfBullOk and (not isStrict or emaF > emaS or patCBull)
bool trendGateBear = htfBearOk and (not isStrict or emaF < emaS or patCBear)
bool strGateBull = not isStrict or structDir == 1 or patCBull
bool strGateBear = not isStrict or structDir == -1 or patCBear

bool rangeBlock = inRange and (rangeBeh == "Block All Signals In Range" or (rangeBeh == "Boundary Only" and midRange))

var int lastSigBar = na
bool cooldownOk = na(lastSigBar) or bar_index - lastSigBar >= cooldown
bool baseOk = enabled and barstate.isconfirmed and sessOk and not highVol and not rangeBlock and cooldownOk

bool bullReady = baseOk and showBuys and patBull and rsiOkBull and volGateBull and trendGateBull and strGateBull and scoreBull >= minScoreEff
bool bearReady = baseOk and showSells and patBear and rsiOkBear and volGateBear and trendGateBear and strGateBear and scoreBear >= minScoreEff
int sigDir = bullReady and not bearReady ? 1 : bearReady and not bullReady ? -1 : 0

// ═══════════════════════════════════════  8) TRADE LIFECYCLE ENGINE  ═══════════════════════════════════════
var int    tDir = 0
var int    tStat = 0            // 0 = idle, 1 = pending (limit order not filled), 2 = active
var bool   tFilled = false
var float  tEntry = na
var float  tStop = na
var float  tTp1 = na
var float  tTp2 = na
var float  tTp3 = na
var float  tTp4 = na
var float  tRisk = na
var float  tScore = na
var int    tHit = 0
var int    tSigBar = na
var int    tActBar = na
var string tText = "No Setup Yet"
var string tRRtxt = "-"
var string tLastSt = ""

var int sN = 0
var int sBuy = 0
var int sSell = 0
var int sFilled = 0
var int sTp1 = 0
var int sTp2 = 0
var int sTp3 = 0
var int sTp4 = 0
var int sSl = 0
var int sExp = 0
var int sInv = 0
var int sUnf = 0
var int sAmb = 0
var int sWin = 0
var float sSumR = 0.0

var box   bxRisk = na
var box   bxRew = na
var line  lnGlow = na
var line  lnEntry = na
var line  lnSl = na
var line  lnT1 = na
var line  lnT2 = na
var line  lnT3 = na
var line  lnT4 = na
var label lbEntry = na
var label lbSl = na
var label lbT1 = na
var label lbT2 = na
var label lbT3 = na
var label lbT4 = na
var array<box>   aBox = array.new<box>()
var array<line>  aLine = array.new<line>()
var array<label> aLbl = array.new<label>()
var int nDrawn = 0

bool evtBuy = false
bool evtSell = false
bool evtTp1 = false
bool evtTp2 = false
bool evtTp3 = false
bool evtTp4 = false
bool evtSl = false
bool evtInv = false
bool evtExp = false
string closeTxt = ""
float exitPx = na
bool closedByOpp = false

// ---------- Trade management (confirmed bars only; levels are never recalculated) ----------
if barstate.isconfirmed and tStat != 0
    if tStat == 1 and bar_index > tSigBar
        bool filled = tDir == 1 ? low <= tEntry : high >= tEntry
        bool tpMissed = tDir == 1 ? high >= tTp1 : low <= tTp1
        if filled
            tStat := 2
            tFilled := true
            tActBar := bar_index
            sFilled += 1
            bool stopSame = tDir == 1 ? low <= tStop : high >= tStop
            if stopSame
                closeTxt := "STOP HIT"
                exitPx := tStop
                sAmb += 1
        else if tpMissed or bar_index - tSigBar >= pendExp
            closeTxt := tpMissed ? "INVALIDATED" : "EXPIRED"
            exitPx := na
    if tStat == 2 and closeTxt == "" and not na(tActBar) and bar_index > tActBar
        bool stopHit = tDir == 1 ? low <= tStop : high >= tStop
        float nxt = tHit == 0 ? tTp1 : tHit == 1 ? tTp2 : tHit == 2 ? tTp3 : tHit == 3 ? tTp4 : na
        bool nxtHit = not na(nxt) and (tDir == 1 ? high >= nxt : low <= nxt)
        if stopHit
            closeTxt := "STOP HIT"
            exitPx := tStop
            if nxtHit
                sAmb += 1   // stop and target inside one candle: conservative = stop first
        else
            for k = 1 to 4
                if tHit == k - 1
                    float lv = k == 1 ? tTp1 : k == 2 ? tTp2 : k == 3 ? tTp3 : tTp4
                    bool hitK = tDir == 1 ? high >= lv : low <= lv
                    if hitK
                        tHit := k
                        if k == 1
                            evtTp1 := true
                        if k == 2
                            evtTp2 := true
                        if k == 3
                            evtTp3 := true
                        if k == 4
                            evtTp4 := true
                        label lbk = k == 1 ? lbT1 : k == 2 ? lbT2 : k == 3 ? lbT3 : lbT4
                        label.set_text(lbk, f_lbl("TP" + str.tostring(k), lv, tDir * (lv - tEntry) / tRisk) + " ✔")
            if tHit == 4
                closeTxt := "TP4 HIT"
                exitPx := tTp4
            else if expiryBars > 0 and bar_index - tActBar >= expiryBars
                closeTxt := "EXPIRED"
                exitPx := close
            else if invalOpp and tHit == 0 and ((tDir == 1 and bearBrkEvt) or (tDir == -1 and bullBrkEvt))
                closeTxt := "INVALIDATED"
                exitPx := close
    if closeTxt == "" and sigDir != 0 and sigDir != tDir and policy != "Ignore New Signals"
        closeTxt := "INVALIDATED"
        exitPx := tStat == 2 ? close : na
        closedByOpp := true

// ---------- Finalise a closed setup: statistics + drawings ----------
if closeTxt != ""
    if tFilled
        float rExit = (not na(exitPx) and tRisk > 0) ? tDir * (exitPx - tEntry) / tRisk : 0.0
        float sumHit = 0.0
        for k = 1 to 4
            if k <= tHit
                float lvk = k == 1 ? tTp1 : k == 2 ? tTp2 : k == 3 ? tTp3 : tTp4
                sumHit += tDir * (lvk - tEntry) / tRisk
        // Equal-split model: 4 equal parts, one per target; the remaining parts exit at rExit.
        float realized = (sumHit + (4 - tHit) * rExit) / 4.0
        sSumR += realized
        if realized > 0
            sWin += 1
        sTp1 += tHit >= 1 ? 1 : 0
        sTp2 += tHit >= 2 ? 1 : 0
        sTp3 += tHit >= 3 ? 1 : 0
        sTp4 += tHit >= 4 ? 1 : 0
        if closeTxt == "STOP HIT"
            sSl += 1
        if closeTxt == "EXPIRED"
            sExp += 1
        if closeTxt == "INVALIDATED"
            sInv += 1
    else
        sUnf += 1
    evtSl := closeTxt == "STOP HIT"
    evtInv := closeTxt == "INVALIDATED"
    evtExp := closeTxt == "EXPIRED"
    int xe = bar_index
    line.set_x2(lnGlow, xe)
    line.set_x2(lnEntry, xe)
    line.set_x2(lnSl, xe)
    line.set_x2(lnT1, xe)
    line.set_x2(lnT2, xe)
    line.set_x2(lnT3, xe)
    line.set_x2(lnT4, xe)
    box.set_right(bxRisk, xe)
    box.set_right(bxRew, xe)
    label.set_x(lbEntry, xe + 1)
    label.set_x(lbSl, xe + 1)
    label.set_x(lbT1, xe + 1)
    label.set_x(lbT2, xe + 1)
    label.set_x(lbT3, xe + 1)
    label.set_x(lbT4, xe + 1)
    if closeTxt == "STOP HIT"
        label.set_text(lbSl, f_lbl("SL", tStop, -1.0) + " ✖")
    tText := closeTxt
    tStat := 0
    tFilled := false

// ---------- Create a new setup ----------
if tStat == 0 and sigDir != 0 and barstate.isconfirmed and not (closedByOpp and policy == "Close On Opposite Signal")
    int d = sigDir
    bool pA = d == 1 ? patABull : patABear
    bool pB = d == 1 ? patBBull : patBBear
    bool pC = d == 1 ? patCBull : patCBear
    bool pD = d == 1 ? patDBull : patDBear
    bool pending = entryMethod == "Breakout Level" and pA and not pB and not pC and not pD
    float lvlBrk = d == 1 ? bkLvlUp : bkLvlDn
    float entry = math.round_to_mintick(pending and not na(lvlBrk) ? lvlBrk : close)
    if pending and na(lvlBrk)
        pending := false
    float structLv = d == 1 ? (na(lastSL) ? lowLow3 : math.min(lastSL, lowLow3)) : (na(lastSH) ? highHigh3 : math.max(lastSH, highHigh3))
    float tickBuf = minTickBuf * syminfo.mintick
    float atrStop = entry - d * stopAtrMult * atrV
    float stopRaw = stopMethod == "ATR" ? atrStop : structLv - d * (stopMethod == "Hybrid" ? stopBuf * atrV : 0.0) - d * tickBuf
    if stopMethod == "Hybrid" and d * (entry - stopRaw) > maxStopAtr * atrV
        stopRaw := atrStop
    float minStop = math.max(minStopAtr * atrV, 2 * syminfo.mintick)
    if d * (entry - stopRaw) < minStop
        stopRaw := entry - d * minStop
    float stopP = math.round_to_mintick(stopRaw)
    float risk = d * (entry - stopP)
    bool stopValid = not na(risk) and risk > 0 and risk >= syminfo.mintick and risk <= maxStopAtr * atrV + syminfo.mintick
    if stopValid
        array<float> lvArr = d == 1 ? shArr : slArr
        float t1 = math.round_to_mintick(f_tgt(d, entry, risk, tp1m, entry, lvArr, tpMode, atrV))
        float t2 = math.round_to_mintick(f_tgt(d, entry, risk, tp2m, t1, lvArr, tpMode, atrV))
        float t3 = math.round_to_mintick(f_tgt(d, entry, risk, tp3m, t2, lvArr, tpMode, atrV))
        float t4 = math.round_to_mintick(f_tgt(d, entry, risk, tp4m, t3, lvArr, tpMode, atrV))
        bool ordered = d * (t1 - entry) > 0 and d * (t2 - t1) > 0 and d * (t3 - t2) > 0 and d * (t4 - t3) > 0
        float rr1 = d * (t1 - entry) / risk
        float rr2 = d * (t2 - entry) / risk
        float rr3 = d * (t3 - entry) / risk
        float rr4 = d * (t4 - entry) / risk
        if ordered and rr2 >= minRR
            float scr = d == 1 ? scoreBull : scoreBear
            tDir := d
            tStat := pending ? 1 : 2
            tFilled := not pending
            tEntry := entry
            tStop := stopP
            tTp1 := t1
            tTp2 := t2
            tTp3 := t3
            tTp4 := t4
            tRisk := risk
            tScore := scr
            tHit := 0
            tSigBar := bar_index
            tActBar := pending ? na : bar_index
            lastSigBar := bar_index
            tRRtxt := "1:" + str.tostring(rr2, "#.0") + " (TP2) | 1:" + str.tostring(rr4, "#.0") + " (TP4)"
            tText := pending ? "PENDING" : "ACTIVE"
            tLastSt := ""
            sN += 1
            if pending == false
                sFilled += 1
            if d == 1
                sBuy += 1
                evtBuy := true
            else
                sSell += 1
                evtSell := true

            // ---- history management: keep at most histKeep completed setups + the active one ----
            if nDrawn >= histKeep + 1
                for j = 1 to 2
                    if array.size(aBox) > 0
                        box.delete(array.shift(aBox))
                for j = 1 to 7
                    if array.size(aLine) > 0
                        line.delete(array.shift(aLine))
                for j = 1 to 6
                    if array.size(aLbl) > 0
                        label.delete(array.shift(aLbl))
                nDrawn -= 1

            // ---- draw the setup ----
            color mainCol = d == 1 ? buyCol : sellCol
            int x0 = bar_index + boxW
            int lsz = 0
            string lblSz = f_size(lblSizeIn)
            string lstl = f_style(lineStyleIn)
            color glowC = glowOn ? color.new(entryCol, 82) : na
            bxRisk := box.new(bar_index, math.max(entry, stopP), x0, math.min(entry, stopP), border_color = color.new(slCol, 70), bgcolor = color.new(slCol, boxTransp), force_overlay = true)
            bxRew := box.new(bar_index, math.max(entry, t4), x0, math.min(entry, t4), border_color = color.new(mainCol, 70), bgcolor = color.new(mainCol, boxTransp), force_overlay = true)
            lnGlow := line.new(bar_index, entry, x0, entry, color = glowC, width = lineW + 4, force_overlay = true)
            lnEntry := line.new(bar_index, entry, x0, entry, color = entryCol, style = line.style_solid, width = lineW + 1, force_overlay = true)
            lnSl := line.new(bar_index, stopP, x0, stopP, color = slCol, style = lstl, width = lineW, force_overlay = true)
            lnT1 := line.new(bar_index, t1, x0, t1, color = showTp1 ? tpCol : na, style = lstl, width = lineW, force_overlay = true)
            lnT2 := line.new(bar_index, t2, x0, t2, color = showTp2 ? tpCol : na, style = lstl, width = lineW, force_overlay = true)
            lnT3 := line.new(bar_index, t3, x0, t3, color = showTp3 ? tpCol : na, style = lstl, width = lineW, force_overlay = true)
            lnT4 := line.new(bar_index, t4, x0, t4, color = showTp4 ? tpCol : na, style = lstl, width = lineW, force_overlay = true)
            lbEntry := label.new(x0, entry, showLabels ? (pending ? "ENTRY (limit)  " : "ENTRY  ") + str.tostring(entry, format.mintick) : "", style = label.style_label_left, color = showLabels ? color.new(entryCol, 10) : na, textcolor = showLabels ? C_NAVY : na, size = lblSz, force_overlay = true)
            lbSl := label.new(x0, stopP, showLabels ? f_lbl("SL", stopP, -1.0) : "", style = label.style_label_left, color = showLabels ? color.new(slCol, 10) : na, textcolor = showLabels ? color.white : na, size = lblSz, force_overlay = true)
            lbT1 := label.new(x0, t1, showLabels and showTp1 ? f_lbl("TP1", t1, rr1) : "", style = label.style_label_left, color = showLabels and showTp1 ? color.new(tpCol, 10) : na, textcolor = showLabels and showTp1 ? color.white : na, size = lblSz, force_overlay = true)
            lbT2 := label.new(x0, t2, showLabels and showTp2 ? f_lbl("TP2", t2, rr2) : "", style = label.style_label_left, color = showLabels and showTp2 ? color.new(tpCol, 10) : na, textcolor = showLabels and showTp2 ? color.white : na, size = lblSz, force_overlay = true)
            lbT3 := label.new(x0, t3, showLabels and showTp3 ? f_lbl("TP3", t3, rr3) : "", style = label.style_label_left, color = showLabels and showTp3 ? color.new(tpCol, 10) : na, textcolor = showLabels and showTp3 ? color.white : na, size = lblSz, force_overlay = true)
            lbT4 := label.new(x0, t4, showLabels and showTp4 ? f_lbl("TP4", t4, rr4) : "", style = label.style_label_left, color = showLabels and showTp4 ? color.new(tpCol, 10) : na, textcolor = showLabels and showTp4 ? color.white : na, size = lblSz, force_overlay = true)
            array.push(aBox, bxRisk)
            array.push(aBox, bxRew)
            array.push(aLine, lnGlow)
            array.push(aLine, lnEntry)
            array.push(aLine, lnSl)
            array.push(aLine, lnT1)
            array.push(aLine, lnT2)
            array.push(aLine, lnT3)
            array.push(aLine, lnT4)
            array.push(aLbl, lbEntry)
            array.push(aLbl, lbSl)
            array.push(aLbl, lbT1)
            array.push(aLbl, lbT2)
            array.push(aLbl, lbT3)
            array.push(aLbl, lbT4)
            nDrawn += 1

// ---------- Per-bar update of an open setup ----------
if barstate.isconfirmed and tStat != 0 and bar_index > tSigBar
    int xr = math.max(bar_index, tSigBar + boxW)
    line.set_x2(lnGlow, xr)
    line.set_x2(lnEntry, xr)
    line.set_x2(lnSl, xr)
    line.set_x2(lnT1, xr)
    line.set_x2(lnT2, xr)
    line.set_x2(lnT3, xr)
    line.set_x2(lnT4, xr)
    box.set_right(bxRisk, xr)
    box.set_right(bxRew, xr)
    label.set_x(lbEntry, xr)
    label.set_x(lbSl, xr)
    label.set_x(lbT1, xr)
    label.set_x(lbT2, xr)
    label.set_x(lbT3, xr)
    label.set_x(lbT4, xr)
    string stNow = tStat == 1 ? "PENDING (limit fill at level)" : tHit == 0 ? "ACTIVE" : "TP" + str.tostring(tHit) + " HIT"
    tText := tStat == 1 ? "PENDING" : tHit == 0 ? "ACTIVE" : "TP" + str.tostring(tHit) + " HIT"
    if stNow != tLastSt
        tLastSt := stNow

// ═══════════════════════════════════════  9) VISUALS - RSI PANEL  ═══════════════════════════════════════
color rsiCol = rsiV >= rsiOb ? C_MAGENTA : rsiV > rsiMid ? (rsiSlope > 0 ? C_CYAN : C_TEAL) : rsiV <= rsiOs ? C_CRIMSON : (rsiSlope < 0 ? C_CRIMSON : C_VIOLET)

h100 = hline(100, "RSI 100", color = color.new(C_SLATE, 100))
hOb  = hline(rsiOb, "Overbought", color = color.new(C_MAGENTA, 40), linestyle = hline.style_dashed)
hUp  = hline(rsiUp, "Upper Momentum", color = color.new(C_TEAL, 65), linestyle = hline.style_dotted)
hMid = hline(rsiMid, "Midpoint", color = color.new(C_SLATE, 30), linestyle = hline.style_dashed)
hLo  = hline(rsiLo, "Lower Momentum", color = color.new(C_CRIMSON, 65), linestyle = hline.style_dotted)
hOs  = hline(rsiOs, "Oversold", color = color.new(C_CRIMSON, 40), linestyle = hline.style_dashed)
h0   = hline(0, "RSI 0", color = color.new(C_SLATE, 100))
fill(hOb, h100, color = color.new(C_MAGENTA, 90), title = "Overbought Zone")
fill(hUp, hOb, color = color.new(C_TEAL, 93), title = "Upper Momentum Zone")
fill(hLo, hUp, color = color.new(C_SLATE, 94), title = "Neutral Zone")
fill(hOs, hLo, color = color.new(C_CRIMSON, 93), title = "Lower Momentum Zone")
fill(h0, hOs, color = color.new(C_CRIMSON, 86), title = "Oversold Zone")

plot(rsiV, "RSI Glow", color = color.new(rsiCol, 82), linewidth = 5)
plot(rsiV, "Zenith RSI", color = rsiCol, linewidth = 2)
plotshape(evtBuy ? rsiV : na, "RSI BUY", location = location.absolute, style = shape.triangleup, color = C_EMERALD, size = size.tiny)
plotshape(evtSell ? rsiV : na, "RSI SELL", location = location.absolute, style = shape.triangledown, color = C_CRIMSON, size = size.tiny)

// ═══════════════════════════════════════  10) VISUALS - SMART VOLUME PANEL  ═══════════════════════════════════════
float volBase  = -45.0
float volBandH = 30.0
float volH = hasVol ? volBandH * math.min(rvol / volCap, 1.0) : 0.0
color volDir = not hasVol ? C_SLATE : rvol < rvolLow ? C_SLATE : close > open ? C_EMERALD : close < open ? C_CRIMSON : C_SLATE
int volTr = not hasVol ? 100 : rvol < rvolLow ? 80 : rvol < rvolThr ? 60 : rvol < rvolStrong ? 38 : rvol < rvolExc ? 15 : 0
color volCol = hasVol and rvol >= rvolExc and volHL ? color.new(C_CYAN, 0) : color.new(volDir, volTr)
hVb0 = hline(-45, "Volume Base", color = color.new(C_SLATE, 70))
hVb1 = hline(-15, "Volume Top", color = color.new(C_SLATE, 100))
fill(hVb0, hVb1, color = color.new(C_NAVY, 80), title = "Volume Panel Background")
plot(volPanel and hasVol ? volBase + volH : na, "Smart Volume (RVOL)", color = volCol, style = plot.style_columns, histbase = -45.0)
plot(volPanel and hasVol ? volBase + volBandH * math.min(1.0 / volCap, 1.0) : na, "Volume Average (RVOL 1.0)", color = color.new(C_AMBER, 20), linewidth = 1)
plot(volPanel and hasVol ? volBase + volBandH * math.min(rvolThr / volCap, 1.0) : na, "RVOL Threshold", color = color.new(C_CYAN, 55), linewidth = 1, style = plot.style_stepline)
plotshape(volPanel and volAnom ? volBase + volBandH + 3 : na, "Volume Anomaly", location = location.absolute, style = shape.diamond, color = C_AMBER, size = size.tiny)

// ═══════════════════════════════════════  11) VISUALS - MAIN CHART (force_overlay)  ═══════════════════════════════════════
plot(emaF, "EMA Fast", color = color.new(C_CYAN, 30), linewidth = 1, display = showEma ? display.pane : display.none, force_overlay = true)
plot(emaS, "EMA Slow", color = color.new(C_VIOLET, 30), linewidth = 1, display = showEma ? display.pane : display.none, force_overlay = true)
plot(emaT, "EMA Trend", color = color.new(C_AMBER, 30), linewidth = 2, display = showEma ? display.pane : display.none, force_overlay = true)

plotshape(evtBuy, "BUY Signal", location = location.belowbar, style = shape.labelup, color = buyCol, textcolor = color.white, text = "BUY", size = size.small, force_overlay = true)
plotshape(evtSell, "SELL Signal", location = location.abovebar, style = shape.labeldown, color = sellCol, textcolor = color.white, text = "SELL", size = size.small, force_overlay = true)
plotshape(showStruct and bosBull and bosOn, "Bull BOS", location = location.belowbar, style = shape.triangleup, color = color.new(C_TEAL, 20), text = "BOS", textcolor = C_TEAL, size = size.tiny, force_overlay = true)
plotshape(showStruct and bosBear and bosOn, "Bear BOS", location = location.abovebar, style = shape.triangledown, color = color.new(C_CRIMSON, 20), text = "BOS", textcolor = C_CRIMSON, size = size.tiny, force_overlay = true)
plotshape(showStruct and chochBull and chochOn, "Bull CHoCH", location = location.belowbar, style = shape.diamond, color = color.new(C_CYAN, 10), text = "CHoCH", textcolor = C_CYAN, size = size.tiny, force_overlay = true)
plotshape(showStruct and chochBear and chochOn, "Bear CHoCH", location = location.abovebar, style = shape.diamond, color = color.new(C_MAGENTA, 10), text = "CHoCH", textcolor = C_MAGENTA, size = size.tiny, force_overlay = true)
// Swing labels sit on the pivot bar; they are only known swLen bars later (confirmation delay).
plotshape(swingLbl and hhNow, "HH", location = location.abovebar, offset = -swLen, style = shape.labeldown, color = color.new(C_SLATE, 50), textcolor = color.white, text = "HH", size = size.tiny, force_overlay = true)
plotshape(swingLbl and lhNow, "LH", location = location.abovebar, offset = -swLen, style = shape.labeldown, color = color.new(C_SLATE, 50), textcolor = color.white, text = "LH", size = size.tiny, force_overlay = true)
plotshape(swingLbl and hlNow, "HL", location = location.belowbar, offset = -swLen, style = shape.labelup, color = color.new(C_SLATE, 50), textcolor = color.white, text = "HL", size = size.tiny, force_overlay = true)
plotshape(swingLbl and llNow, "LL", location = location.belowbar, offset = -swLen, style = shape.labelup, color = color.new(C_SLATE, 50), textcolor = color.white, text = "LL", size = size.tiny, force_overlay = true)

// Data window values
plot(scoreBull, "Bullish Score", display = display.data_window)
plot(scoreBear, "Bearish Score", display = display.data_window)
plot(rvol, "Relative Volume", display = display.data_window)

// ═══════════════════════════════════════  12) SINGLE DASHBOARD (MOVABLE + SCALABLE)  ═══════════════════════════════════════
// One table holds the market dashboard AND the signal statistics. Free placement is emulated with transparent spacer
// cells (sizes are a % of the chart), and size is controlled by the text size in points (Dashboard Size %).
f_anchor(string s) =>
    switch s
        "Top Left" => position.top_left
        "Top Center" => position.top_center
        "Middle Left" => position.middle_left
        "Middle Center" => position.middle_center
        "Middle Right" => position.middle_right
        "Bottom Left" => position.bottom_left
        "Bottom Center" => position.bottom_center
        "Bottom Right" => position.bottom_right
        => position.top_right

bool anchRight  = dashAnchor == "Top Right" or dashAnchor == "Middle Right" or dashAnchor == "Bottom Right"
bool anchBottom = dashAnchor == "Bottom Left" or dashAnchor == "Bottom Center" or dashAnchor == "Bottom Right"
int  dashTxt    = math.max(5, math.round(9.0 * dashScale / 100.0))
int  dRows      = showStats ? 29 : 18
var table dash  = table.new(f_anchor(dashAnchor), 4, dRows, bgcolor = na, border_width = 0, force_overlay = true)

f_cell(table t, int c, int r, string txt, color tc, color bg) =>
    table.cell(t, c, r, txt, text_color = tc, text_halign = text.align_left, text_size = dashTxt, bgcolor = bg)

f_row(table t, int r, string k, string v, color vc) =>
    f_cell(t, 1, r, " " + k + " ", color.new(C_TXT, 25), color.new(C_NAVY, dashTransp))
    f_cell(t, 2, r, " " + v + " ", vc, color.new(C_NAVY, dashTransp))

f_title(table t, int r, string txt) =>
    table.cell(t, 1, r, " " + txt + " ", text_color = C_CYAN, text_halign = text.align_center, text_size = dashTxt, bgcolor = color.new(C_NAVY, math.max(0, dashTransp - 8)))
    table.merge_cells(t, 1, r, 2, r)

int tfSec = timeframe.in_seconds(timeframe.period)
bool tfOff = tfWarn and (tfSec < 60 or tfSec > 3600)

if barstate.islast and showDash
    // spacer cells that implement the horizontal / vertical offsets
    table.cell(dash, 0, 1, "", width = anchRight ? 0.0 : dashOffX, text_size = 1, bgcolor = na)
    table.cell(dash, 3, 1, "", width = anchRight ? dashOffX : 0.0, text_size = 1, bgcolor = na)
    table.cell(dash, 1, 0, "", height = anchBottom ? 0.0 : dashOffY, text_size = 1, bgcolor = na)
    table.cell(dash, 1, dRows - 1, "", height = anchBottom ? dashOffY : 0.0, text_size = 1, bgcolor = na)

    f_title(dash, 1, "ZENITH RSI VOLUME PRO")
    f_row(dash, 2, "Market Regime", regime, regime == "Bullish Trend" or regime == "Bullish Bias" ? C_EMERALD : regime == "Bearish Trend" or regime == "Bearish Bias" ? C_CRIMSON : highVol ? C_AMBER : C_TXT)
    f_row(dash, 3, "RSI State", str.tostring(rsiV, "#.0") + " | " + rsiClass, rsiCol)
    f_row(dash, 4, "Relative Volume", hasVol ? str.tostring(rvol, "#.00") + "x  (avg " + str.tostring(avgVol, format.volume) + ")" : "N/A - no volume data", hasVol ? (rvol >= rvolExc ? C_CYAN : C_TXT) : C_AMBER)
    f_row(dash, 5, "Volume Strength", volClass + (volAbsorb ? " | absorption?" : ""), C_TXT)
    f_row(dash, 6, "Momentum State", momState, strongBullPart or bullMomState ? C_EMERALD : strongBearPart or bearMomState ? C_CRIMSON : C_TXT)
    f_row(dash, 7, "Bullish Score", str.tostring(scoreBull, "#") + " / 100  " + f_rating(scoreBull), scoreBull >= minScoreEff ? C_EMERALD : C_TXT)
    f_row(dash, 8, "Bearish Score", str.tostring(scoreBear, "#") + " / 100  " + f_rating(scoreBear), scoreBear >= minScoreEff ? C_CRIMSON : C_TXT)
    f_row(dash, 9, "Signal Mode", sigMode + " (min " + str.tostring(minScoreEff) + ")", C_TXT)
    f_row(dash, 10, "Current Setup", tText + (tStat != 0 ? (tDir == 1 ? " | LONG" : " | SHORT") : ""), tStat != 0 ? (tDir == 1 ? C_EMERALD : C_CRIMSON) : C_TXT)
    f_row(dash, 11, "Risk-to-Reward", tRRtxt, C_TXT)
    f_row(dash, 12, "Structure", structDir == 1 ? "Bullish" : structDir == -1 ? "Bearish" : "Undefined", structDir == 1 ? C_EMERALD : structDir == -1 ? C_CRIMSON : C_TXT)
    f_row(dash, 13, "HTF Trend (" + htfTf + ")", not htfOn ? "Off" : htfC > htfE ? "Bullish" : htfC < htfE ? "Bearish" : "Neutral", not htfOn ? C_SLATE : htfC > htfE ? C_EMERALD : C_CRIMSON)
    f_row(dash, 14, "Session", not sessOn ? "Filter Off" : sessOk ? "In Session" : "Outside Session", not sessOn ? C_SLATE : sessOk ? C_EMERALD : C_AMBER)
    f_row(dash, 15, "Volume Source", hasVol ? "Tick/Exchange volume (OHLCV inference)" : "Unavailable - volume gates off", hasVol ? C_TXT : C_AMBER)
    f_row(dash, 16, "Timeframe", tfOff ? "Outside M1-H1 (M5 recommended)" : "OK  (" + timeframe.period + ")", tfOff ? C_AMBER : C_TXT)

    if showStats
        float winRate = sFilled > 0 ? 100.0 * sWin / sFilled : na
        float avgR = sFilled > 0 ? sSumR / sFilled : na
        f_title(dash, 17, "SIGNAL STATISTICS (indicator only - not a backtest, no costs)")
        f_row(dash, 18, "Confirmed Setups", str.tostring(sN) + "  (BUY " + str.tostring(sBuy) + " / SELL " + str.tostring(sSell) + ")", C_TXT)
        f_row(dash, 19, "Filled Trades", str.tostring(sFilled), C_TXT)
        f_row(dash, 20, "TP1 / TP2 events", str.tostring(sTp1) + " / " + str.tostring(sTp2), C_TXT)
        f_row(dash, 21, "TP3 / TP4 events", str.tostring(sTp3) + " / " + str.tostring(sTp4), C_TXT)
        f_row(dash, 22, "Stop-Loss", str.tostring(sSl), C_TXT)
        f_row(dash, 23, "Expired / Invalid / Unfilled", str.tostring(sExp) + " / " + str.tostring(sInv) + " / " + str.tostring(sUnf), C_TXT)
        f_row(dash, 24, "Ambiguous (SL+TP one bar)", str.tostring(sAmb) + " (counted as stop)", C_AMBER)
        f_row(dash, 25, "Win Rate (realized R > 0)", na(winRate) ? "-" : str.tostring(winRate, "#.0") + "%", C_TXT)
        f_row(dash, 26, "Average Realized R", na(avgR) ? "-" : str.tostring(avgR, "#.00") + " R", C_TXT)
        f_row(dash, 27, "Total Realized R", str.tostring(sSumR, "#.00") + " R", sSumR >= 0 ? C_EMERALD : C_CRIMSON)

// ═══════════════════════════════════════  13) ALERTS  ═══════════════════════════════════════
bool evtHiBuy  = evtBuy and scoreBull >= hiConf
bool evtHiSell = evtSell and scoreBear >= hiConf
bool evtExcVol = barstate.isconfirmed and hasVol and rvol >= rvolExc

alertcondition(evtBuy, "Zenith: Confirmed BUY", "Zenith RSI Volume Pro: BUY confirmed on {{ticker}} {{interval}} | reference price {{close}}")
alertcondition(evtSell, "Zenith: Confirmed SELL", "Zenith RSI Volume Pro: SELL confirmed on {{ticker}} {{interval}} | reference price {{close}}")
alertcondition(evtHiBuy, "Zenith: High-Confluence BUY", "Zenith RSI Volume Pro: HIGH-CONFLUENCE BUY on {{ticker}} {{interval}} | reference price {{close}}")
alertcondition(evtHiSell, "Zenith: High-Confluence SELL", "Zenith RSI Volume Pro: HIGH-CONFLUENCE SELL on {{ticker}} {{interval}} | reference price {{close}}")
alertcondition(evtTp1, "Zenith: TP1 Reached", "Zenith RSI Volume Pro: TP1 reached on {{ticker}} {{interval}}")
alertcondition(evtTp2, "Zenith: TP2 Reached", "Zenith RSI Volume Pro: TP2 reached on {{ticker}} {{interval}}")
alertcondition(evtTp3, "Zenith: TP3 Reached", "Zenith RSI Volume Pro: TP3 reached on {{ticker}} {{interval}}")
alertcondition(evtTp4, "Zenith: TP4 Reached", "Zenith RSI Volume Pro: TP4 reached on {{ticker}} {{interval}}")
alertcondition(evtSl, "Zenith: Stop-Loss Reached", "Zenith RSI Volume Pro: STOP-LOSS reached on {{ticker}} {{interval}}")
alertcondition(evtInv, "Zenith: Setup Invalidated", "Zenith RSI Volume Pro: setup INVALIDATED on {{ticker}} {{interval}}")
alertcondition(bullDivNow, "Zenith: Bullish RSI Divergence", "Zenith RSI Volume Pro: bullish RSI divergence confirmed on {{ticker}} {{interval}}")
alertcondition(bearDivNow, "Zenith: Bearish RSI Divergence", "Zenith RSI Volume Pro: bearish RSI divergence confirmed on {{ticker}} {{interval}}")
alertcondition(evtExcVol, "Zenith: Exceptional Relative Volume", "Zenith RSI Volume Pro: exceptional relative volume on {{ticker}} {{interval}}")

// Dynamic alert() messages: fire once per confirmed bar and never repeat while a condition merely stays true.
string pxFmt = format.mintick
string sigHead = syminfo.ticker + " " + timeframe.period + " | "
if dynAlerts and (evtBuy or evtSell)
    string dirTxt = evtBuy ? "BUY" : "SELL"
    string msg = "Zenith RSI Volume Pro | " + dirTxt + " | " + sigHead + "Entry ref " + str.tostring(tEntry, pxFmt) + " | SL " + str.tostring(tStop, pxFmt) + " | TP1 " + str.tostring(tTp1, pxFmt) + " | TP2 " + str.tostring(tTp2, pxFmt) + " | TP3 " + str.tostring(tTp3, pxFmt) + " | TP4 " + str.tostring(tTp4, pxFmt) + " | Score " + str.tostring(tScore, "#") + " | RVOL " + (hasVol ? str.tostring(rvol, "#.00") : "n/a") + " | RSI " + str.tostring(rsiV, "#.0")
    alert(msg, alert.freq_once_per_bar_close)
if dynAlerts and (evtTp1 or evtTp2 or evtTp3 or evtTp4 or evtSl or evtInv or evtExp)
    string evTxt = evtSl ? "STOP-LOSS HIT" : evtInv ? "SETUP INVALIDATED" : evtExp ? "SETUP EXPIRED" : evtTp4 ? "TP4 HIT" : evtTp3 ? "TP3 HIT" : evtTp2 ? "TP2 HIT" : "TP1 HIT"
    alert("Zenith RSI Volume Pro | " + evTxt + " | " + sigHead + "Entry " + str.tostring(tEntry, pxFmt) + " | SL " + str.tostring(tStop, pxFmt), alert.freq_once_per_bar_close)
if dynAlerts and (bullDivNow or bearDivNow)
    alert("Zenith RSI Volume Pro | " + (bullDivNow ? "BULLISH" : "BEARISH") + " RSI DIVERGENCE (confirmed) | " + sigHead + "RSI " + str.tostring(rsiV, "#.0"), alert.freq_once_per_bar_close)
if dynAlerts and evtExcVol
    alert("Zenith RSI Volume Pro | EXCEPTIONAL VOLUME | " + sigHead + "RVOL " + str.tostring(rvol, "#.00"), alert.freq_once_per_bar_close)
